Skip to content

feat(recording): add automatic cursor overlay - #42

Merged
shreyaskarnik merged 1 commit into
shreyaskarnik:mainfrom
OleksandrKucherenko:feat/issue-37-cursor-overlay
Aug 22, 2026
Merged

feat(recording): add automatic cursor overlay#42
shreyaskarnik merged 1 commit into
shreyaskarnik:mainfrom
OleksandrKucherenko:feat/issue-37-cursor-overlay

Conversation

@OleksandrKucherenko

@OleksandrKucherenko OleksandrKucherenko commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add opt-in video.cursorHighlight configuration for recording-wide pseudo-cursor feedback
  • inject the cursor overlay when recording starts and restore it after top-level navigation
  • forward cursor settings through the CLI, primary pipeline, and viewport variants
  • document the configuration and cover config, runtime, CLI, recorder, and pipeline behavior with tests

Testing

  • npx tsc --noEmit
  • npx vitest run tests/cursor.test.ts tests/config.test.ts tests/record.test.ts tests/narration.test.ts tests/cli.test.ts
  • npx vitest run tests/pipeline.test.ts -t "forwards automatic cursor highlighting"

Closes #37

@salir-admin

Copy link
Copy Markdown

@shreyaskarnik @jarkkosyrjala please review and approve, waiting for this merge

@shreyaskarnik

Copy link
Copy Markdown
Owner

Thanks a lot for this, @OleksandrKucherenko — really appreciate the contribution! 🙏

This is a clean piece of work: the config → CLI → record() → env-var → runtime bridge is wired through every path (primary pipeline and viewport variants), which is exactly the invariant that usually gets missed, and the test coverage across config/record/CLI/pipeline/narration is genuinely thorough. Nice touch mirroring the existing showActions encoding ('{}' for true, empty string for off) so the two features stay symmetric.

Merging this now. I did spot a few edge cases around the framenavigated reinjection that I'll clean up in a follow-up so you don't have to do another round-trip:

  1. document.body can be null at nav-commit time. Playwright emits framenavigated when the navigation commits, which can be before the parser has produced <body>. cursorHighlight() ends in document.body.appendChild(dot), so on a page with a parser-blocking script/stylesheet in <head> this throws TypeError: Cannot read properties of null — and that message isn't in cursor.ts's disposal-error allowlist, so it warns and the ring is gone for the rest of that page.

  2. Same-document (SPA) navigations also fire framenavigated. Playwright dispatches navigated from frameCommittedSameDocumentNavigation too, so every pushState/hash route change reinjects even though the document was never replaced. Two consequences: the new ring resets to left:-100px and stays invisible until the next mouse event, and cursorHighlight()'s replace path removes the old element without invoking the __cleanup it stashed on it — so each route change leaks a capturing mousemove + click listener on a detached node (N route changes → N stacked ripples per click).

  3. The paint nudge is now serialized behind the cursor evaluate. The original framenavigated listener existed specifically to unstick CDP promptly after page.goto() lands; with the cursor enabled, _triggerPaint() waits on the injection, which lengthens the stale-frame window it was written to shorten.

Follow-up makes the injection idempotent + body-safe and decouples the paint trigger. Also worth noting for posterity: moving this._recordingPage = page out of the legacy else branch means _triggerPaint() now actually runs on the chromium + jpeg-stitch CDP-direct path, where it was previously a silent no-op — that's a latent bug fixed by accident here, and a welcome one. 🎉

Thanks again!

@shreyaskarnik
shreyaskarnik merged commit 1f97a93 into shreyaskarnik:main Aug 22, 2026
4 checks passed
@shreyaskarnik

Copy link
Copy Markdown
Owner

Follow-up with the fixes is up: #43 — covers all three points above (null document.body at nav-commit, same-document reinjection + listener leak, and decoupling the paint nudge). Thanks again for the feature, @OleksandrKucherenko! 🚀

shreyaskarnik added a commit that referenced this pull request Aug 22, 2026
Follow-up to #42. Three issues in the `framenavigated` reinjection path:

1. `document.body` can be null at navigation commit. Playwright emits
   `framenavigated` when the nav commits, which can precede the parser
   producing <body> (a render/parser-blocking resource in <head> widens
   the window). `cursorHighlight()` ended in `document.body.appendChild`,
   throwing `TypeError: Cannot read properties of null` — a message that
   is not in cursor.ts's disposal-error allowlist, so it surfaced as a
   warning and left the page permanently cursor-less. Injection now
   defers to DOMContentLoaded when body is absent, with a generation
   counter so a later call supersedes a queued install.

2. Same-document navigations reinjected needlessly. Playwright dispatches
   `navigated` from `frameCommittedSameDocumentNavigation` too, so every
   pushState/hash route change rebuilt the ring: it reset to left:-100px
   (invisible until the next mouse event) and the replace path removed
   the element without invoking the `__cleanup` stored on it, leaking a
   capturing mousemove + click listener per route change (N leaks => N
   stacked ripples per click). Adds an internal `ensureCursorHighlight()`
   that no-ops when a ring is present or queued; the replace path now
   runs the previous cleanup.

3. The paint nudge was chained behind the cursor evaluate. That listener
   exists to unstick CDP promptly after a navigation, and the evaluate
   blocks on the new document's execution context. Paint now fires
   immediately and again once the cursor lands.

`ensureCursorHighlight` is deliberately not re-exported from index.ts —
public API surface is unchanged.

Tests execute the injected browser function against a DOM stub, so the
injection logic itself is covered rather than just its arguments. Each
new test was verified to fail against the pre-fix implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

overlay with pseudo mouse support

4 participants